'use strict'

// var utils = require('axios/lib/utils')
// var settle = require('axios/lib/core/settle')
// var buildFullPath = require('axios/lib/core/buildFullPath')
// var buildURL = require('axios/lib/helpers/buildURL')
// var http = require('http')
// var https = require('https')
// var httpFollow = require('follow-redirects').http
// var httpsFollow = require('follow-redirects').https
// var url = require('url')
// var zlib = require('zlib')
// var VERSION = require('axios/lib/env/data').version
// var createError = require('axios/lib/core/createError')
// var enhanceError = require('axios/lib/core/enhanceError')
// var defaults = require('axios/lib/defaults')
// var Cancel = require('axios/lib/cancel/Cancel')

import * as http from 'http'
import * as https from 'https'
import * as url from 'url'
import * as zlib from 'zlib'

import followRedirects from './follow-redirects.js'

import utils from 'axios/lib/utils'
import settle from 'axios/lib/core/settle'
import buildFullPath from 'axios/lib/core/buildFullPath'
import buildURL from 'axios/lib/helpers/buildURL'
import {version as VERSION} from 'axios/lib/env/data'
import createError from 'axios/lib/core/createError'
import enhanceError from 'axios/lib/core/enhanceError'
import defaults from 'axios/lib/defaults'
import Cancel from 'axios/lib/cancel/Cancel'

var isHttps = /https:?/

const {http: httpFollow, https: httpsFollow} = followRedirects

/**
 *
 * @param {http.ClientRequestArgs} options
 * @param {AxiosProxyConfig} proxy
 * @param {string} location
 */
/* istanbul ignore next */
function setProxy(options, proxy, location) {
  options.hostname = proxy.host
  options.host = proxy.host
  options.port = proxy.port
  options.path = location

  // Basic proxy authorization
  if (proxy.auth) {
    var base64 = Buffer.from(proxy.auth.username + ':' + proxy.auth.password, 'utf8').toString('base64')
    options.headers['Proxy-Authorization'] = 'Basic ' + base64
  }

  // If a proxy is used, any redirects must also pass through the proxy
  options.beforeRedirect = function beforeRedirect(redirection) {
    redirection.headers.host = redirection.host
    setProxy(redirection, proxy, redirection.href)
  }
}

/*eslint consistent-return:0*/
/* istanbul ignore next */
export default function httpAdapter(config) {
  return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {
    var onCanceled
    function done() {
      if (config.cancelToken) {
        config.cancelToken.unsubscribe(onCanceled)
      }

      if (config.signal) {
        config.signal.removeEventListener('abort', onCanceled)
      }
    }
    var resolve = function resolve(value) {
      done()
      resolvePromise(value)
    }
    var reject = function reject(value) {
      done()
      rejectPromise(value)
    }
    var data = config.data
    var headers = config.headers
    var headerNames = {}

    Object.keys(headers).forEach(function storeLowerName(name) {
      headerNames[name.toLowerCase()] = name
    })

    // Set User-Agent (required by some servers)
    // See https://github.com/axios/axios/issues/69
    if ('user-agent' in headerNames) {
      // User-Agent is specified; handle case where no UA header is desired
      if (!headers[headerNames['user-agent']]) {
        delete headers[headerNames['user-agent']]
      }
      // Otherwise, use specified value
    } else {
      // Only set header if it hasn't been set in config
      headers['User-Agent'] = 'axios/' + VERSION
    }

    if (data && !utils.isStream(data)) {
      if (Buffer.isBuffer(data)) {
        // Nothing to do...
      } else if (utils.isArrayBuffer(data)) {
        data = Buffer.from(new Uint8Array(data))
      } else if (utils.isString(data)) {
        data = Buffer.from(data, 'utf-8')
      } else {
        return reject(
          createError('Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream', config),
        )
      }

      // Add Content-Length header if data exists
      if (!headerNames['content-length']) {
        headers['Content-Length'] = data.length
      }
    }

    // HTTP basic authentication
    var auth = undefined
    if (config.auth) {
      var username = config.auth.username || ''
      var password = config.auth.password || ''
      auth = username + ':' + password
    }

    // Parse url
    var fullPath = buildFullPath(config.baseURL, config.url)
    var parsed = url.parse(fullPath)
    var protocol = parsed.protocol || 'http:'

    if (!auth && parsed.auth) {
      var urlAuth = parsed.auth.split(':')
      var urlUsername = urlAuth[0] || ''
      var urlPassword = urlAuth[1] || ''
      auth = urlUsername + ':' + urlPassword
    }

    if (auth && headerNames.authorization) {
      delete headers[headerNames.authorization]
    }

    var isHttpsRequest = isHttps.test(protocol)
    var agent = isHttpsRequest ? config.httpsAgent : config.httpAgent

    var options = {
      path: buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''),
      method: config.method.toUpperCase(),
      headers: headers,
      agent: agent,
      agents: {http: config.httpAgent, https: config.httpsAgent},
      auth: auth,
    }

    if (config.socketPath) {
      options.socketPath = config.socketPath
    } else {
      options.hostname = parsed.hostname
      options.port = parsed.port
    }

    var proxy = config.proxy
    if (!proxy && proxy !== false) {
      var proxyEnv = protocol.slice(0, -1) + '_proxy'
      var proxyUrl = process.env[proxyEnv] || process.env[proxyEnv.toUpperCase()]
      if (proxyUrl) {
        var parsedProxyUrl = url.parse(proxyUrl)
        var noProxyEnv = process.env.no_proxy || process.env.NO_PROXY
        var shouldProxy = true

        if (noProxyEnv) {
          var noProxy = noProxyEnv.split(',').map(function trim(s) {
            return s.trim()
          })

          shouldProxy = !noProxy.some(function proxyMatch(proxyElement) {
            if (!proxyElement) {
              return false
            }
            if (proxyElement === '*') {
              return true
            }
            if (
              proxyElement[0] === '.' &&
              parsed.hostname.substr(parsed.hostname.length - proxyElement.length) === proxyElement
            ) {
              return true
            }

            return parsed.hostname === proxyElement
          })
        }

        if (shouldProxy) {
          proxy = {
            host: parsedProxyUrl.hostname,
            port: parsedProxyUrl.port,
            protocol: parsedProxyUrl.protocol,
          }

          if (parsedProxyUrl.auth) {
            var proxyUrlAuth = parsedProxyUrl.auth.split(':')
            proxy.auth = {
              username: proxyUrlAuth[0],
              password: proxyUrlAuth[1],
            }
          }
        }
      }
    }

    if (proxy) {
      options.headers.host = parsed.hostname + (parsed.port ? ':' + parsed.port : '')
      setProxy(
        options,
        proxy,
        protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path,
      )
    }

    var transport
    var isHttpsProxy = isHttpsRequest && (proxy ? isHttps.test(proxy.protocol) : true)
    if (config.transport) {
      transport = config.transport
    } else if (config.maxRedirects === 0) {
      transport = isHttpsProxy ? https : http
    } else {
      if (config.maxRedirects) {
        options.maxRedirects = config.maxRedirects
      }
      transport = isHttpsProxy ? httpsFollow : httpFollow
    }

    if (config.maxBodyLength > -1) {
      options.maxBodyLength = config.maxBodyLength
    }

    if (config.insecureHTTPParser) {
      options.insecureHTTPParser = config.insecureHTTPParser
    }

    // Create the request
    var req = transport.request(options, function handleResponse(res) {
      if (req.aborted) return

      // uncompress the response body transparently if required
      var stream = res

      // return the last request in case of redirects
      var lastRequest = res.req || req

      // if no content, is HEAD request or decompress disabled we should not decompress
      if (res.statusCode !== 204 && lastRequest.method !== 'HEAD' && config.decompress !== false) {
        switch (res.headers['content-encoding']) {
          /*eslint default-case:0*/
          case 'gzip':
          case 'compress':
          case 'deflate':
            // add the unzipper to the body stream processing pipeline
            stream = stream.pipe(zlib.createUnzip())

            // remove the content-encoding in order to not confuse downstream operations
            delete res.headers['content-encoding']
            break
        }
      }

      var response = {
        status: res.statusCode,
        statusText: res.statusMessage,
        headers: res.headers,
        config: config,
        request: lastRequest,
      }

      if (config.responseType === 'stream') {
        response.data = stream
        settle(resolve, reject, response)
      } else {
        var responseBuffer = []
        var totalResponseBytes = 0
        stream.on('data', function handleStreamData(chunk) {
          responseBuffer.push(chunk)
          totalResponseBytes += chunk.length

          // make sure the content length is not over the maxContentLength if specified
          if (config.maxContentLength > -1 && totalResponseBytes > config.maxContentLength) {
            stream.destroy()
            reject(
              createError(
                'maxContentLength size of ' + config.maxContentLength + ' exceeded',
                config,
                null,
                lastRequest,
              ),
            )
          }
        })

        stream.on('error', function handleStreamError(err) {
          if (req.aborted) return
          reject(enhanceError(err, config, null, lastRequest))
        })

        stream.on('end', function handleStreamEnd() {
          var responseData = Buffer.concat(responseBuffer)
          if (config.responseType !== 'arraybuffer') {
            responseData = responseData.toString(config.responseEncoding)
            if (!config.responseEncoding || config.responseEncoding === 'utf8') {
              responseData = utils.stripBOM(responseData)
            }
          }

          response.data = responseData
          settle(resolve, reject, response)
        })
      }
    })

    // Handle errors
    req.on('error', function handleRequestError(err) {
      if (req.aborted && err.code !== 'ERR_FR_TOO_MANY_REDIRECTS') return
      reject(enhanceError(err, config, null, req))
    })

    // Handle request timeout
    if (config.timeout) {
      // This is forcing a int timeout to avoid problems if the `req` interface doesn't handle other types.
      var timeout = parseInt(config.timeout, 10)

      if (isNaN(timeout)) {
        reject(createError('error trying to parse `config.timeout` to int', config, 'ERR_PARSE_TIMEOUT', req))

        return
      }

      // Sometime, the response will be very slow, and does not respond, the connect event will be block by event loop system.
      // And timer callback will be fired, and abort() will be invoked before connection, then get "socket hang up" and code ECONNRESET.
      // At this time, if we have a large number of request, nodejs will hang up some socket on background. and the number will up and up.
      // And then these socket which be hang up will devoring CPU little by little.
      // ClientRequest.setTimeout will be fired on the specify milliseconds, and can make sure that abort() will be fired after connect.
      req.setTimeout(timeout, function handleRequestTimeout() {
        req.abort()
        var transitional = config.transitional || defaults.transitional
        reject(
          createError(
            'timeout of ' + timeout + 'ms exceeded',
            config,
            transitional.clarifyTimeoutError ? 'ETIMEDOUT' : 'ECONNABORTED',
            req,
          ),
        )
      })
    }

    if (config.cancelToken || config.signal) {
      // Handle cancellation
      // eslint-disable-next-line func-names
      onCanceled = function (cancel) {
        if (req.aborted) return

        req.abort()
        reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel)
      }

      config.cancelToken && config.cancelToken.subscribe(onCanceled)
      if (config.signal) {
        config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled)
      }
    }

    // Send the request
    if (utils.isStream(data)) {
      data.on('error', function handleStreamError(err) {
        reject(enhanceError(err, config, null, req))
      })

      /************************************/
      //node 支持上传进度
      let loaded = 0
      let currentReq = req._currentRequest || req
      data.on('data', function (chunk) {
        if (!currentReq.write(chunk)) {
          //判断写缓冲区是否写满(node的官方文档有对write方法返回值的说明)
          data.pause() //如果写缓冲区不可用，暂停读取数据
        }
        loaded += chunk.length

        //进度
        if (typeof config.onUploadProgress == 'function') {
          config.onUploadProgress({
            loaded,
            total: data.length,
          })
        }
      })

      data.on('end', function () {
        currentReq.end()
      })

      currentReq.on('drain', function () {
        //写缓冲区可用，会触发"drain"事件
        data.resume() //重新启动读取数据
      })
      /************************************/
    } else {
      req.end(data)
    }
  })
}
